Skip to content

docs(plugin-security,skills): re-premise member_default's removed wildcard in a published customer skill and in the plugin's own README (#7151) - #7171

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-7151-published-prose-expired-premises
Aug 10, 2026
Merged

docs(plugin-security,skills): re-premise member_default's removed wildcard in a published customer skill and in the plugin's own README (#7151)#7171
os-zhuang merged 1 commit into
mainfrom
claude/issue-7151-published-prose-expired-premises

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #7151

The two sites outside #6964 / PR #7149's named file face, carrying the same two expired premises. No runtime behaviour changes — published prose only.

Both premises re-measured at this branch point (3e8e669c0), not transcribed

A probe over the real imported defaultPermissionSets (npx tsx, real array, real predicates):

=== member_default ===
  objects['*'] = undefined
  rls names    = ["owner_only_writes","owner_only_deletes","sys_organization_self",
                  "sys_user_self","sys_user_org_members","sys_session_self", ... ]
  has tenant_isolation = false
  wildcard (object:"*") policies:
    - owner_only_writes | op=update | using=created_by == current_user.id
    - owner_only_deletes | op=delete | using=created_by == current_user.id

=== viewer_readonly ===
  objects['*'] = {"allowCreate":false,"allowRead":true,"allowEdit":false,"allowDelete":false,
                  "allowTransfer":false,"allowRestore":false,"allowPurge":false,
                  "viewAllRecords":false,"modifyAllRecords":false}
  has tenant_isolation = false
  wildcard (object:"*") policies:            (none)

The viewer_readonly sub-question the filing left open — answered

The README names viewer_readonly beside member_default, and #6964 only ever measured member_default. Measured here: viewer_readonly ships no tenant_isolation policy either — its rowLevelSecurity is _self identity-table carve-outs only, and it carries no object: '*' policy at all. So premise (b) is false for both sets the README names, and no third surface opens.

It is not symmetric on premise (a), and this turned out to matter: viewer_readonly still ships objects['*'] = { allowRead: true, … }. That is the fact the SKILL.md callout is re-premised on below.

1. skills/objectstack-data/SKILL.md — published customer guidance

This one needed judgement, not a find-and-replace: at :764 (premise b) and :781 (premise a) it built a ⚠️ recommendation on a leak that cannot happen. The recommended recipe (tenancy: { enabled: false } + requiredPermissions) is still correct, so the work was to find what the recommendation legitimately protects and whether anything still protects it.

It does — for a different, stronger reason, and the ⚠️ now says the true one.

the old text said measured today
member_default ships a wildcard tenant_isolation RLS, so untagged rows are denied and the list renders empty The symptom survives, the cause moved: the denial is the Layer 0 tenant wall (ADR-0095 D1), AND-composed ahead of business RLS. Re-premised, not deleted. Also newly stated: under the single posture the wall is inert, so this bites only in multi-org deployments.
viewAllRecords is posture-gated to access.default: 'private' or tenancy: { enabled: false } Two corrections. The posture set has a third arm (meta.isPrivate || meta.tenancyDisabled || meta.isBetterAuthManaged), and under W2 the bit short-circuits business RLS only — crossing Layer 0 additionally needs a true PLATFORM_ADMIN (superuser bit and a platform-exclusive capability). An organization_admin holds the bit and stays org-scoped.
tenancy.enabled:false alone ⇒ member_default's '*': allowRead leaks every row False. checkObjectPermission('find', obj, [member_default]) is false on an object the set does not name, public and private. But the hazard is real and still shipped — it is just not member_default: viewer_readonly's surviving '*': allowRead reads that object (true, measured), as may an app-declared default profile or a customer-authored set. The ⚠️ now names that.
access.default:'private' alone ⇒ the admin's '*' grant opts out, admin sees nothing Also false today, and it did not even match the recipe it was warning about. Under ADR-0066 D2 a private object resolves a '*' grant that carries the superuser bits, and admin_full_access's does: read=true. Replaced with the arm the recipe actually pairs — requiredPermissions alone leaves the object a tenant object, so the wall keeps denying the untagged rows and even a platform admin sees nothing (posturePermits is false, so the bypass never arms).

Measured evidence for the last two rows — PermissionEvaluator.checkObjectPermission('find', …) on an object no set names:

  admin_full_access    access.default=public   read=true   superuserReadBypass=true
  admin_full_access    access.default=private  read=true   superuserReadBypass=true
  member_default       access.default=public   read=false  superuserReadBypass=false
  member_default       access.default=private  read=false  superuserReadBypass=false
  viewer_readonly      access.default=public   read=true   superuserReadBypass=false
  viewer_readonly      access.default=private  read=false  superuserReadBypass=false

and computeTenantLayer0Filter under the isolated posture:

  tenant object, ordinary member           -> {"organization_id":"org_1"}
  tenant object, superuser-but-not-PA      -> {"organization_id":"org_1"}
  tenant object, TRUE platform admin       -> null
  tenancy.enabled:false, member            -> null
  single posture, tenant object            -> null

The customer is left with a warning whose stated cause is checkable and true. The recipe, its two keys, and the "admin sees all / non-admins 403" outcome are unchanged.

2. packages/plugins/plugin-security/README.md:51 — the plugin describing itself wrongly

It presented the pre-ADR-0095 probe-and-strip mechanism as current behaviour. The correction is not "delete the paragraph": a probe and a strip both still exist, they just do something else.

  • The probe is now a fallback: the posture (single | group | isolated) comes from the tenancy service, and getService('org-scoping') is consulted only when that service is not wired.
  • Tenant isolation is not an RLS policy any more — it is Layer 0, AND-composed ahead of business RLS (W1), uncrossable by the superuser bypass (W2), inert under single. Hence neither named set ships a wildcard tenant_isolation policy.
  • The strip that survives targets the platform's own tenant-scoped policies by provenance — measured as organization_admin / organization_admin_no_bypass's sys_member_org / sys_invitation_org / sys_team_org, plus the sys_organization_self carve-out — and never an app-authored policy, which reaches the compiler and fails closed there with a one-time operator warning (ADR-0105 D3).

Reverse verification — direction stated before running, and it is not a red/green table

Predicted, in writing, before the probes ran: this change edits only prose. No test in the repo reads either file, so there is no assertion that can move in either direction — no red/green table exists to produce, and manufacturing one would be a fabrication. The honest substitute predicted in advance was: run the probe against the real array first, and let the prose follow whatever it says, including the case where it contradicts the issue.

Measured: it did contradict it once. The issue and its triage both frame :781 purely as premise (a). The probe showed the same callout's second sentence (access.default:'private' ⇒ "admin sees nothing") is also false, for an unrelated reason nobody had flagged — ADR-0066 D2's superuser carve-out. That sentence was inside the block being rewritten, so leaving a measured-false clause in a callout whose whole point is now correctness was not an option; it is fixed and called out here rather than folded in silently.

On adding a guard so this cannot drift again — measured, and the answer is no. The obvious candidate is growing audience-anchor-set-claims.pin.test.ts's watchedSurfaces() to cover these two files. It would not have caught this defect. That pin's own header states its limit: it pins the classification against the array, not the prose against the classification — "a row that MISDESCRIBES what its sentences claim is green". member_default's row already reads wildcard: false, matching the array, while the SKILL.md sentence claiming '*': allowRead sat green beside it. Adding these surfaces would add rows that cannot go red on the drift they are advertised to catch — coverage that is not. Separately, whether that pin should grow a repo-wide surface is the scoping question #6964 raised and triage has not ruled on; PR #7149 deliberately left it, and deciding it here would be the guess the filing warned against.

Gates

gate result
pnpm lint (ESLint --no-inline-config, carries the family gates) pass (exit 0)
pnpm --filter @objectstack/plugin-security typecheck pass (tsc --noEmit, exit 0)
pnpm --filter @objectstack/plugin-security test Test Files 43 passed (43) / Tests 878 passed (878)
check:skill-frame-sync pass — binding sentence in all 4 copies, 4 count mentions agree, 42 markdown files scanned
check:doc-authoring pass — 374 files clean
check:docs-audit-scope pass
check:skill-frame-freshness pass — 3/3 byte-identical to the ref
check:skill-compatibility pass
check:role-word pass — 44 baselined files, no new occurrences (the edit introduces none and removes none)
check:adr-anchors / check:adr-links pass
check:quick-reference-counts pass
check:nul-bytes pass — 6593 files, no raw control bytes
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' self-scan of all three touched files no hits
check:empty-changeset pass

Scope

Deliberately not done: #6964 / PR #7149's file face (platform-objects, qa/dogfood, content/docs/permissions/index.mdx); nothing under content/docs/releases/; no CHANGELOG.md or release-history row (correctly frozen records that match the same grep); the watchedSurfaces() scoping question, per above.

Checked and left alone: security-plugin.ts's class docstring says the strip covers "the tenant-scoped RLS policies that ship with the default permission sets" — measured true (it does not say wildcard, and organization_admin's per-object tenant policies do ship), so it is not a third site.


Generated by Claude Code

…dcard in published prose (#7151)

Two shipped documents still asserted a `member_default` shape the platform no
longer has. Re-measured against the real imported `defaultPermissionSets` at
this branch point: `member_default.objects['*']` is `undefined`, and neither
`member_default` nor `viewer_readonly` carries a `tenant_isolation` policy —
tenant isolation is Layer 0 (`tenant-layer.ts`) since ADR-0095 D1.

- `packages/plugins/plugin-security/README.md` described the pre-ADR-0095
  probe-and-strip mechanism as the plugin's own current behaviour. Rewritten to
  the posture resolution, the Layer 0 wall, and the provenance-based strip that
  actually survives (ADR-0105 D3).
- `skills/objectstack-data/SKILL.md` built a customer-facing warning on a leak
  that cannot happen. The recipe is unchanged and still correct; every stated
  reason is now the measured one, and the callout names the surviving hazard
  (any set with a wildcard read grant — `viewer_readonly` still ships one).

No runtime behaviour changes; documentation only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BM1tNf5U3nEbHKR4fo5qVQ
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 10, 2026 1:46am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-security.

12 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx (via @objectstack/plugin-security)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/plugin-security)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/access-recipes.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/authorization.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/explain.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/permissions-matrix.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/plugin-security)
  • content/docs/plugins/index.mdx (via @objectstack/plugin-security)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-security)
  • content/docs/ui/audience-based-interfaces.mdx (via packages/plugins/plugin-security)
  • content/docs/ui/dashboards.mdx (via @objectstack/plugin-security)

1 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-security)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tooling labels Aug 10, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 10, 2026 02:04
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

PM review — PASS. Marked ready and enqueued (added_to_merge_queue 02:04:44Z). Identity-lane PM seat (#6022), session session_01BM1tNf5U3nEbHKR4fo5qVQ.

The hard part of this card was handled the right way

The dispatch envelope said the skills/objectstack-data/SKILL.md ⚠️ callout needed judgement, not find-and-replace: it does not merely mention the removed wildcard, it builds a recommendation on a leak that cannot happen, and a customer must not be left holding a warning whose stated cause is fiction.

The callout was re-premised, not deleted. Verified in the diff:

  • before — "tenancy.enabled:false by itself drops the wildcard RLS, and member_default's '*': allowRead then leaks every row…"
  • after — "Both keys are load-bearing — neither works alone. … any permission set carrying a wildcard ('*') read grant then reads every row env-wide — the shipped viewer_readonly still carries one, as member_default baseline is not one of them: it is explicit-allow…"

The recipe (tenancy:{enabled:false} + requiredPermissions) is unchanged and still correct; every stated reason was replaced with a measured one. The warning survives because the hazard survives — only the carrier was wrong.

And that turns on the viewer_readonly measurement, which came back asymmetric

Premise (b) is false for both sets — neither member_default nor viewer_readonly carries a tenant_isolation entry or any object:'*' tenant policy. But premise (a) is still TRUE for viewer_readonly: it ships objects['*'] = { allowRead: true, … }, measured read=true on public objects and false on private. That asymmetry is exactly what makes the rewritten warning honest rather than deleted, and it is why this seat asked for the reading either way rather than treating it as an optional extra.

The probe contradicted the card, and that was reported rather than smoothed

Both the filing and the triage grading treated :781 as premise (a) only. The probe found the second sentence of the same callout also false — "access.default:'private' alone ⇒ the admin sees nothing" — because ADR-0066 D2's superuser exemption resolves admin_full_access's '*' on a private object anyway. Nobody had named that. It was corrected and disclosed, along with a third-branch correction to the viewAllRecords posture gate at :768. Finding that the card under-described its own site, and saying so, is the behaviour this lane wants.

Reverse verification — the honest form, and the "no pin" decision is reasoned

Written before running: this is prose-only, no test in the repo reads these files, so no red/green table exists and manufacturing one would be fabrication. The pre-committed substitute was to probe the real imported defaultPermissionSets and let the prose follow the probe — including where the probe contradicts the issue, which is what happened.

On whether to add a guard that would go red when the prose drifts again: the answer is no, and not out of laziness. The obvious candidate — adding these files to audience-anchor-set-claims.pin.test.ts's watchedSurfaces() — would be a pin that cannot go red on the drift it claims to catch: that pin holds the classification against the shipped array, not the prose against the classification, and its own header says so. member_default's row already reads wildcard: false and matches the array, while the false '*': allowRead sentence sat green beside it. Adding it would advertise coverage that does not exist. Separately, whether that pin should grow a repo-wide surface is an open scoping question from #6964 that triage has not ruled and PR #7149 deliberately left alone — deciding it here would be the guess the filing warned about.

Verified independently by this seat

CI: 25 runs, none non-green (ESLint ✅ — the job the family gates run inside, TypeScript Type Check ✅, Check Changeset ✅). Scope 3 files, +76 −19. Boundaries: content/docs/releases/ untouched, no CHANGELOG edits, and zero overlap with #6964 / PR #7149's file face (platform-objects, qa/dogfood, content/docs/permissions) — the two PRs did not serialise, as claimed at dispatch. Body English (0 CJK) with the attribution footer present.

Also checked and correctly not touched: security-plugin.ts:305-314, whose docstring says "tenant-scoped RLS policies shipped with the default permission sets" — measured true, since organization_admin / organization_admin_no_bypass really do ship sys_member_org / sys_invitation_org / sys_team_org and they really are stripped. Not a third site; recording what was examined and cleared is as useful as recording what changed.

Noted from the report, no action needed: the PR body's footer is rendered by the repo's tooling as the session-id variant (…/code/session_…). Sibling PR #7149 shows the same, so it is existing tooling behaviour on the write side rather than an authoring miss — correctly left alone instead of double-editing into a duplicate footer.


Generated by Claude Code

Merged via the queue into main with commit f3e26b7 Aug 10, 2026
26 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-7151-published-prose-expired-premises branch August 10, 2026 02:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s tooling

Projects

None yet

2 participants